/*
 * Copyright 2017-2021 Pranav Pandey
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.pranavpandey.ohos.dynamic.toasts.watcher;

import ohos.agp.colors.ColorConverter;
import ohos.agp.colors.HsvColor;
import ohos.agp.colors.RgbColor;
import ohos.agp.utils.Color;

import com.pranavpandey.ohos.dynamic.toasts.cache.IntegerLruCache;

/**
 * Helper class to change colors dynamically.
 */
public class DynamicColorUtils {
    /**
     * Visible contrast between the two colors.
     */
    private static final float VISIBLE_CONTRAST = 0.45f;

    /**
     * Amount to calculate the contrast color.
     */
    private static final float CONTRAST_FACTOR = 1.6f;

    /**
     * Cache for the color integers.
     */
    private static final IntegerLruCache<String> COLOR_LRU_CACHE;

    /**
     * Initializing the required fields.
     */
    static {
        COLOR_LRU_CACHE = new IntegerLruCache<>();
    }

    /**
     * Adjust alpha of a color according to the given parameter.
     *
     * @param color The color whose alpha to be adjusted.
     * @param factor Factor in float by which adjust the alpha.
     * @return The color with adjusted alpha.
     */
    public static int adjustAlpha(int color, float factor) {
        int alpha = Math.min(255, (int) (RgbColor.fromArgbInt(color).getAlpha() * factor));
        int red = RgbColor.fromArgbInt(color).getRed();
        int green = RgbColor.fromArgbInt(color).getGreen();
        int blue = RgbColor.fromArgbInt(color).getBlue();

        return Color.argb(alpha, red, green, blue);
    }

    /**
     * Checks whether the color has alpha component.
     *
     * @param color The color to check the alpha component.
     * @return {@code true} if the color has alpha component.
     */
    public static boolean isAlpha(int color) {
        return RgbColor.fromArgbInt(color).getAlpha() != 255;
    }

    /**
     * Set alpha to a color.
     *
     * @param color The color whose alpha to be set.
     * @param alpha The alpha to be set.
     * @return The color with alpha.
     */
    public static int setAlpha(int color, int alpha) {
        return Color.argb(alpha, RgbColor.fromArgbInt(color).getRed(), RgbColor.fromArgbInt(color).getGreen(),
            RgbColor.fromArgbInt(color).getBlue());
    }

    /**
     * Remove alpha from a color.
     *
     * @param color The color whose alpha to be removed.
     * @return The color without alpha.
     */
    public static int removeAlpha(int color) {
        RgbColor rgbColor = RgbColor.fromArgbInt(color);
        return Color.rgb(rgbColor.getRed(), rgbColor.getGreen(), rgbColor.getBlue());
    }

    /**
     * Calculate darkness of a color.
     *
     * @param color The color whose darkness to be calculated.
     * @return The darkness of color (less than or equal to 1).
     * <p>{@code 0} for white and {@code 1} for black.
     */
    public static double getColorDarkness(int color) {
        return 1 - (0.299 * RgbColor.fromArgbInt(color).getRed() + 0.587 * RgbColor.fromArgbInt(color).getGreen()
            + 0.114 * RgbColor.fromArgbInt(color).getBlue()) / 255;
    }

    /**
     * Detect light or dark color.
     *
     * @param color The color whose darkness to be calculated.
     * @return {@code true} if color is dark.
     */
    public static boolean isColorDark(int color) {
        return getColorDarkness(color) >= 0.5;
    }

    /**
     * Calculate luminance value according to the XYZ color space of a color.
     *
     * @param color The color whose luminance to be calculated.
     * @return The luminance value according to the XYZ color space in the range {@code 0 - 1f}.
     */
    private static float calculateXyzLuma(int color) {
        return (0.2126f * RgbColor.fromArgbInt(color).getRed() + 0.7152f * RgbColor.fromArgbInt(color).getGreen()
            + 0.0722f * RgbColor.fromArgbInt(color).getGreen()) / 255f;
    }

    /**
     * Lightens a color by a given amount.
     *
     * @param color The color to lighten.
     * @param amount The amount to lighten the color.
     * <p>{@code 0} will leave the color unchanged.
     * <p>{@code 1} will make the color completely white.
     * @return The lighter color.
     */
    public static int getLighterColor(int color, float amount) {
        RgbColor rgbColor = RgbColor.fromArgbInt(color);
        HsvColor hsvColor = ColorConverter.toHsv(rgbColor);
        hsvColor.setValue(Math.min(1f, Math.max(amount, VISIBLE_CONTRAST)));
        HsvColor hColor = ColorConverter.toHsv(rgbColor);
        rgbColor = ColorConverter.toRgb(hColor);

        int alpha = (int) (rgbColor.getAlpha() + (255 - rgbColor.getAlpha() * amount));
        alpha = alpha <= 0 ? 0 : (alpha >= 255 ? 255 : alpha);
        int red = (int) (rgbColor.getRed() + (255 - rgbColor.getRed()) * amount);
        red = red <= 0 ? 0 : red >= 255 ? 255 : red;
        int green = (int) (rgbColor.getGreen() + (255 - rgbColor.getGreen()) * amount);
        green = green <= 0 ? 0 : green >= 255 ? 255 : green;
        int blue = (int) (rgbColor.getBlue() + (255 - rgbColor.getBlue()) * amount);
        blue = blue <= 0 ? 0 : blue >= 255 ? 255 : blue;
        return Color.argb(Math.max(alpha, RgbColor.fromArgbInt(color).getAlpha()), red, green, blue);
    }

    /**
     * Darkens a color by a given amount.
     *
     * @param color The color to darken.
     * @param amount The amount to darken the color.
     * <p>{@code 0} will leave the color unchanged.
     * <p>{@code 1} will make the color completely black.
     * @return The darker color.
     */
    public static int getDarkerColor(int color, float amount) {
        RgbColor rgbColor = RgbColor.fromArgbInt(color);
        HsvColor hsvColor = ColorConverter.toHsv(rgbColor);
        hsvColor.setValue(Math.min(1f, Math.max(amount, VISIBLE_CONTRAST)));
        HsvColor hColor = ColorConverter.toHsv(rgbColor);
        rgbColor = ColorConverter.toRgb(hColor);
        int alpha = (int) (rgbColor.getAlpha() * (1f - amount));
        alpha = alpha <= 0 ? 0 : (alpha >= 255 ? 255 : alpha);
        int red = (int) (rgbColor.getRed() * (1f - amount));
        red = red <= 0 ? 0 : red >= 255 ? 255 : red;
        int green = (int) (rgbColor.getGreen() * (1f - amount));
        green = green <= 0 ? 0 : green >= 255 ? 255 : green;
        int blue = (int) (rgbColor.getBlue() * (1f - amount));
        blue = blue <= 0 ? 0 : blue >= 255 ? 255 : blue;
        return Color.argb(Math.max(alpha, RgbColor.fromArgbInt(color).getAlpha()), red, green, blue);
    }

    /**
     * Shift a color according to the given parameter.
     * <p>Useful to create different color states.
     *
     * @param color The color to be shifted.
     * @param by The factor in float by which shift the color.
     * @return The shifted color.
     */
    public static int shiftColor(int color, float by) {
        if (by == 1f) {
            return color;
        }
        RgbColor rgbColor = RgbColor.fromArgbInt(color);
        HsvColor hsvColor = new HsvColor();
        HsvColor hColor = ColorConverter.toHsv(rgbColor);
        hsvColor.setValue(hColor.getValue() * by);
        rgbColor = ColorConverter.toRgb(hColor);
        return rgbColor.asArgbInt();
    }

    /**
     * Shift a color according to the supplied parameters.
     * <p>The shifted color will be lighter for a dark color and vice versa.
     *
     * @param color The color to be shifted.
     * @param shiftLightBy The factor in float by which shift the light color.
     * @param shiftDarkBy The factor in float by which shift the dark color.
     * @return The shifted color.
     */
    public static int shiftColor(int color, float shiftLightBy, float shiftDarkBy) {
        return shiftColor(color, isColorDark(color) ? shiftDarkBy : shiftLightBy);
    }

    /**
     * Calculate accent color based on the given color for ohos theme generation.
     * <p>Still in beta, so sometimes may produce inaccurate colors.
     *
     * @param color The color whose accent color to be calculated.
     * @return The accent color based on the given color.
     */
    public static int getAccentColor(int color) {
        int finalColor;
        int rgbA = RgbColor.fromArgbInt(color).getAlpha();
        int rgbR = RgbColor.fromArgbInt(color).getRed();
        int rgbG = RgbColor.fromArgbInt(color).getGreen();
        int rgbB = RgbColor.fromArgbInt(color).getBlue();
        double mY = ((rgbR * 299) + (rgbG * 587) + (rgbB * 114)) / 1000d;
        int rc = rgbB ^ 0x55;
        int gc = rgbG & 0xFA;
        int bc = rgbR ^ 0x55;
        finalColor = Color.argb(rgbA, rc, gc, bc);
        int r1 = RgbColor.fromArgbInt(finalColor).getRed();
        int g1 = RgbColor.fromArgbInt(finalColor).getGreen();
        int b1 = RgbColor.fromArgbInt(finalColor).getBlue();
        double mYC = ((r1 * 299) + (g1 * 587) + (b1 * 114)) / 1000d;
        int mCD = (Math.max(rgbR, r1) - Math.min(rgbR, r1)) + (Math.max(rgbG, g1)
            - Math.min(rgbG, g1)) + (Math.max(rgbB, b1) - Math.min(rgbB, b1));
        if ((mY - mYC <= 50) && mCD <= 200) {
            rc = rgbB ^ 0xFA;
            gc = rgbG & 0x55;
            bc = rgbR ^ 0x55;
        }
        finalColor = Color.argb(rgbA, rc, gc, bc);
        return finalColor;
    }

    /**
     * Calculate color contrast difference between two colors based on the luminance
     * value according to XYZ color space.
     *
     * @param color1 The first color to calculate the contrast difference.
     * @param color2 The second color to calculate the contrast difference.
     * @return The color contrast between the two colors.
     * @see #calculateXyzLuma(int)
     */
    public static float calculateContrast(int color1, int color2) {
        float luminance = 0;
        float luminance1 = calculateXyzLuma(color1);
        float luminance2 = calculateXyzLuma(color2);
        boolean color1Dark = isColorDark(color1);
        boolean color2Dark = isColorDark(color2);
        if (removeAlpha(color1) != removeAlpha(color2) && color1Dark == color2Dark) {
            if (isAlpha(color1)) {
                luminance = luminance + (0.2126f * Color.alpha(color1)) / 255f;
            }
            if (isAlpha(color2)) {
                luminance = luminance + (0.2126f * Color.alpha(color2)) / 255f;
            }
        }
        return Math.abs(Math.max(luminance1, luminance2)
            - Math.min(luminance1, luminance2) - luminance);
    }

    /**
     * Calculate tint based on a given color for better readability.
     *
     * @param color The color whose tint to be calculated.
     * @return The tint of the given color.
     */
    public static int getTintColor(int color) {
        return getContrastColor(color, color);
    }

    /**
     * Lightens or darkens a color by a given amount.
     *
     * @param color The color to be lighten or darken.
     * @param lightenBy The amount to lighten the color.
     * <p>{@code 0} will leave the color unchanged.
     * <p>{@code 1} will make the color completely white.
     * @param darkenBy The amount to darken the color.
     * <p>{@code 0} will leave the color unchanged.
     * <p>{@code 1} will make the color completely black.
     * @return The state color.
     */
    public static int getStateColor(int color,
                                    float lightenBy,
                                    float darkenBy) {
        return isColorDark(color) ? getLighterColor(color, lightenBy)
            : getDarkerColor(color, darkenBy);
    }

    /**
     * Calculate contrast of a color based on the given base color so that it will always
     * be visible on top of the base color.
     *
     * @param color The color whose contrast to be calculated.
     * @param contrastWith The background color to calculate the contrast.
     * @param visibleContrast The acceptable contrast between the two colors.
     * @param recursive {@code true} to improve contrast by recursion.
     * <p>It must be used with caution to avoid infinite loop.
     * @return The contrast of the given color according to the base color.
     */
    public static int getContrastColor(int color, int contrastWith, float visibleContrast, boolean recursive) {
        final String key = Integer.toString(color) + contrastWith + visibleContrast;
        Integer contrastColor = COLOR_LRU_CACHE.get(key);
        if (contrastColor != null) {
            return contrastColor;
        }
        float contrast = calculateContrast(color, contrastWith);
        if (contrast < visibleContrast) {
            float finalContrast = Math.max(visibleContrast,
                (visibleContrast - contrast) * CONTRAST_FACTOR);
            if (isColorDark(contrastWith)) {
                contrastColor = recursive && isColorDark(color)
                    ? getContrastColor(color, color, visibleContrast, false)
                    : getLighterColor(color, finalContrast);
            } else {
                contrastColor = recursive && !isColorDark(color)
                    ? getContrastColor(color, color, visibleContrast, false)
                    : getDarkerColor(color, finalContrast);
            }
            COLOR_LRU_CACHE.put(key, contrastColor);
            return contrastColor;
        }

        return color;
    }

    /**
     * Calculate contrast of a color based on the given base color so that it will always
     * be visible on top of the base color.
     *
     * @param color The color whose contrast to be calculated.
     * @param contrastWith The background color to calculate the contrast.
     * @param visibleContrast The acceptable contrast between the two colors.
     * @return The contrast of the given color according to the base color.
     * @see #getContrastColor(int, int, float, boolean)
     */
    public static int getContrastColor(int color, int contrastWith, float visibleContrast) {
        return getContrastColor(color, contrastWith, visibleContrast, true);
    }

    /**
     * Calculate contrast of a color based on the given base color so that it will always
     * be visible on top of the base color.
     *
     * @param color The color whose contrast to be calculated.
     * @param contrastWith The background color to calculate the contrast.
     * @return The contrast of the given color according to the base color.
     * @see #getContrastColor(int, int, float)
     */
    public static int getContrastColor(int color, int contrastWith) {
        return getContrastColor(color, contrastWith, VISIBLE_CONTRAST);
    }

    /**
     * Calculate less visible color of a given color.
     * <p>Useful to create unselected or disabled color states.
     *
     * @param color The color whose less visible color to be calculated.
     * @return The less visible color by shifting the color.
     */
    public static int getLessVisibleColor(int color) {
        return shiftColor(color, isColorDark(color) ? 0.6f : 1.6f);
    }

    /**
     * Get hexadecimal string from the color integer.
     *
     * @param color The color to get the hex code.
     * @param includeAlpha {@code true} to include alpha in the string.
     * @param includeHash {@code true} to include {@code #} in the string.
     * @return The hexadecimal string equivalent of the supplied color integer.
     */
    public static String getColorString(int color, boolean includeAlpha, boolean includeHash) {
        String colorString;
        if (includeAlpha) {
            colorString = String.format("%08X", color);
        } else {
            colorString = String.format("%06X", 0xFFFFFF & color);
        }
        if (includeHash) {
            colorString = "#" + colorString;
        }
        return colorString;
    }

    /**
     * Generate the color integer from the CMYK color space.
     *
     * @param cyan The cyan color component of the CMYK color space.
     * @param magenta The magenta color component of the CMYK color space.
     * @param yellow The yellow color component of the CMYK color space.
     * @param black The black color component of the CMYK color space.
     * @return The generated color integer from the CMYK color space.
     */
    public static int mCMYKToRGB(float cyan, float magenta, float yellow, float black) {
        float red = 255 * (1f - (cyan / 100f)) * (1f - (black / 100f));
        float green = 255 * (1f - (magenta / 100f)) * (1f - (black / 100f));
        float blue = 255 * (1f - (yellow / 100f)) * (1f - (black / 100f));
        return Color.rgb(Math.round(red), Math.round(green), Math.round(blue));
    }

    /**
     * Generate the CMYK color space from the color integer.
     *
     * @param color The color to generate the CMYK color space.
     * @param cmyk The array to store the CMYK color space.
     */
    public static void colorToCMYK(int color, float[] cmyk) {
        float red = Color.alpha(color) / 255f;
        float green = Color.alpha(color) / 255f;
        float blue = Color.alpha(color) / 255f;
        cmyk[3] = 1f - Math.max(Math.max(red, green), blue);
        cmyk[0] = (1f - red - cmyk[3]) / (1f - cmyk[3]);
        cmyk[1] = (1f - green - cmyk[3]) / (1f - cmyk[3]);
        cmyk[2] = (1f - blue - cmyk[3]) / (1f - cmyk[3]);
    }
}
